package au.edu.educationau.opensource.dsm.adapters.querygenerator;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Iterator;
import java.util.Map;

import org.apache.log4j.Logger;
import org.apache.commons.lang.StringUtils;

import au.edu.educationau.opensource.dsm.adapters.SearchAdapterProperties;
import au.edu.educationau.opensource.dsm.obj.SearchCriteria;
import au.edu.educationau.opensource.dsm.service.PropertiesService;
import au.edu.educationau.opensource.dsm.worker.VocabMapper;


/**
 * Generates a SOLR query from DSM query parameters.
 * 
 * Much of the SOLR query parameters (eg. qf, qt, facet.XXX) can be passed as DSM custom params
 * (using the form: adapterCode.solrParam=value) and/or specified as adapter configuration
 * properites.  See code for details.
 * 
 * Field level searching can be performed by passing DSM custom params of the form
 * adapterCode.field.dsmFieldName=value. The dsmFieldName is crosswalked to a SOLR field name using
 * the adapter configuration properties of the form: adapterCode.metadata.crosswalk.dsmField=solrField
 * (these is the reverse process of the crosswalk done by SolrContentHandler).
 * 
 * In addition the actual values to search for in the SOLR field can be mapped to a DSM custom param
 * value using the VocabMapper.  This is invoked by specifying an apapter configuration property of the
 * form adapterCode.metadata.crosswalk.dsmField.useVocab=true.  See VocabMapper service for details of
 * how the actual vocab map is specified.  
 * 
 * @author vhobbs, skahl
 * 
 */
public class SolrQueryGenerator implements QueryGenerator {
	static Logger logger = Logger.getLogger(SolrQueryGenerator.class.getName());

	private final String KEYWORD_CONSTRAINT_PHRASE = "phrase";
	private final String KEYWORD_CONSTRAINT_OR = "any";
	private final String KEYWORD_CONSTRAINT_AND = "all";

	private final String REQUEST_HANDLER_DISMAX = "dismax";
	private final String REQUEST_HANDLER_DEFAULT = "dismax";

	/*
	 * A value unlikely to occur in SOLR index, used to ensure a search returns
	 * no results.
	 */
	private final String UNLIKELY_VALUE = "asdfh32974187g3hjbvj01pwerhamxcv87a3423";

	private Map crosswalkMapping = null;

	/**
	 * Implementing classes will use the search criteria and adapter properties
	 * to generate the search URL
	 * 
	 * @return the search URL
	 */
	public String generateQueryParams(SearchCriteria criteria, SearchAdapterProperties props) {
		StringBuffer queryString = new StringBuffer();

		String sortMethod = PropertiesService.getStringProperty(props.getAdapterCode() + ".request.sortMethod", null);
		if (sortMethod != null) {
			queryString.append("&sort=" + sortMethod);
		}

		String fieldConstraint = criteria.getCustomParamValue("field");
		if (fieldConstraint != null && !"".equals(fieldConstraint)) {
			fieldConstraint = fieldConstraint + ":";
		} else {
			fieldConstraint = "";
		}

		String rowLimit = criteria.getCustomParamValue(props.getAdapterCode() + ".rows");
		if (rowLimit != null && rowLimit.length() > 0) {
			queryString.append("&rows=" + rowLimit);
		}

		String[] queryFields = criteria.getCustomParamValues(props.getAdapterCode() + ".qf");
		boolean qfSpecified = queryFields != null && queryFields.length > 0 && queryFields[0] != null && !"".equals(queryFields[0]);
		if (qfSpecified) {
			queryString.append("&qf=" + queryFields[0]);
		}

		String requestHandler = criteria.getCustomParamValue(props.getAdapterCode() + ".qt");
		if (requestHandler == null || "".equals(requestHandler)) {
			if (qfSpecified) {
				requestHandler = REQUEST_HANDLER_DISMAX;
			} else {
				requestHandler = PropertiesService.getStringProperty(props.getAdapterCode() + ".request.handler", null);
				if (requestHandler == null) {
					requestHandler = REQUEST_HANDLER_DEFAULT;
				}
			}
		}
		queryString.append("&qt=" + requestHandler);

		String qValue = fieldConstraint + au.edu.educationau.opensource.dsm.util.StringUtils.arrayToString(criteria.getQueryArray(), "%20");
		if (criteria.getKeywordConstraint().equals(KEYWORD_CONSTRAINT_PHRASE)) {
			// generate phrase query
			logger.debug("building phrase query");
			queryString.append("&q=\"" + qValue + "\"");
		} else if (criteria.getKeywordConstraint().equals(KEYWORD_CONSTRAINT_OR)) {
			// generate disjunctive query
			logger.debug("building OR query");
			queryString.append("&q=" + qValue);
			queryString.append("&q.op=OR");
		} else if (criteria.getKeywordConstraint().equals(KEYWORD_CONSTRAINT_AND)) { // assume
																						// AND
			// generate conjunctive query
			logger.debug("building AND query");
			queryString.append("&q=" + qValue);
			queryString.append("&q.op=AND");
		}

		String customFieldFilters = buildCustomFieldFilters(criteria, props);
		if (customFieldFilters.length() > 0) {
			logger.debug("query built for custom field filters: " + customFieldFilters);
			queryString.append("&fq=").append(customFieldFilters);
		}

		queryString.append("&fl=score&facet=true");

		String[] facetQueries = criteria.getCustomParamValues(props.getAdapterCode() + ".facet.query");
		logger.debug("facetQueries: " + facetQueries);
		for (int i = 0; facetQueries != null && i < facetQueries.length; i++) {
			queryString.append("&facet.query=" + facetQueries[i]);
		}
		String[] facetField = criteria.getCustomParamValues(props.getAdapterCode() + ".facet.field");
		logger.debug("facetField: " + facetField);
		for (int i = 0; facetField != null && i < facetField.length; i++) {
			queryString.append("&facet.field=" + facetField[i]);
		}
		String[] filterQueries = criteria.getCustomParamValues(props.getAdapterCode() + ".fq");
		logger.debug("filterQueries: " + filterQueries);
		for (int i = 0; filterQueries != null && i < filterQueries.length; i++) {
			queryString.append("&fq=" + filterQueries[i].replaceAll(" ", "%20"));
		}
		String[] facetLimit = criteria.getCustomParamValues(props.getAdapterCode() + ".facet.limit");
		logger.debug("facetLimit: " + facetLimit);
		for (int i = 0; facetLimit != null && i < facetLimit.length; i++) {
			queryString.append("&facet.limit=" + facetLimit[i]);
		}
		logger.debug("queryString is: " + queryString.toString());
		return queryString.toString();
	}

	/**
	 * Build field level filters as required by search custom params.
	 * 
	 * Finds all custom params like adapterCode.field.fieldName=value and builds
	 * them into a SOLR query like mappedFieldName:(vocabValue1 OR vocabValue2
	 * ...).
	 * 
	 * mappedFieldName comes from the metadata.crosswalk mapping defined for the
	 * adapter. vocabValues come from the VocabMapper service.
	 * 
	 * @return a SOLR query, or an empty String
	 */
	private String buildCustomFieldFilters(SearchCriteria criteria, SearchAdapterProperties props) {
		StringBuffer solrQuery = new StringBuffer();

		Map fieldParams = criteria.getCustomParamValuesByPrefix(props.getAdapterCode() + ".field.");
		if (!fieldParams.isEmpty()) {
			for (Iterator it = fieldParams.entrySet().iterator(); it.hasNext();) {
				Map.Entry entry = (Map.Entry) it.next();
				String fieldName = (String) entry.getKey();

				// decode fieldName using crosswalk mapping defined in
				// properties file
				if (crosswalkMapping == null) {
					crosswalkMapping = PropertiesService.getAllProperties(props.getAdapterCode() + ".metadata.crosswalk");
				}

				String mappedFieldName = (String) crosswalkMapping.get(fieldName);
				String useVocab = null;
				if (mappedFieldName != null) {
					useVocab = (String) crosswalkMapping.get(fieldName + ".usevocab");
				} else {
					mappedFieldName = (String) crosswalkMapping.get("metadata." + fieldName);
					if (mappedFieldName != null) {
						useVocab = (String) crosswalkMapping.get("metadata." + fieldName + ".usevocab");
					}
				}

				if (mappedFieldName != null) {
					String[] fieldParamValues = (String[]) entry.getValue();
					for (int i = 0; i < fieldParamValues.length; i++) {
						String fieldFilter = fieldParamValues[i];
						if ("true".equals(useVocab)) {
							fieldFilter = buildVocabFilterForField(props, fieldName, fieldFilter);
						}
						if (fieldFilter != null) {
							try {
								/* SOLR uses default op = OR for fq values, so explicitly make it AND here.
								 * It doesn't matter if the filter is a phrase search using quotes, as SOLR
								 * ignores the AND keyword in this case anyway.
								 */
								fieldFilter = StringUtils.join(StringUtils.split(fieldFilter), " AND ");
								
								fieldFilter = URLEncoder.encode(fieldFilter, "UTF-8");
								if (solrQuery.length() > 0) {
									solrQuery.append("%20");
								}
								solrQuery.append(mappedFieldName);
								solrQuery.append(":(");
								solrQuery.append(fieldFilter);
								solrQuery.append(")");
							} catch (UnsupportedEncodingException e) {
							}
						}
					}
				} else {
					// there is no schema field mapped to the requested field
					// - ie. no results should match this filter
					return UNLIKELY_VALUE;
				}
			}
		}
		return solrQuery.toString();
	}

	/**
	 * Returns SOLR syntax for a field level filter using the VocabMapper, or
	 * null if no field level search is to be performed.
	 */
	private String buildVocabFilterForField(SearchAdapterProperties props, String fieldName, String value) {
		String[] vocabValues = VocabMapper.getValuesForKey(props.getAdapterCode(), fieldName, value);

		if (vocabValues == null) {
			// there is no vocab mapping - ie. no results should match this
			// filter
			return UNLIKELY_VALUE;
		}

		StringBuffer sb = new StringBuffer("(");
		for (int i = 0; i < vocabValues.length; i++) {
			if (VocabMapper.ALL_VALUES.equals(vocabValues[i])) {
				// vocab mapping says all values should match this filter, ie.
				// no filter to be applied
				return null;
			}
			if (i > 0)
				sb.append(" OR ");
			sb.append("\"").append(vocabValues[i]).append("\"");
		}
		sb.append(")");

		return sb.toString();
	}
}
